feat: zaino-store — block store with LMDB persistence, Lean formaliza…#1378
feat: zaino-store — block store with LMDB persistence, Lean formaliza…#1378hhanh00 wants to merge 1 commit into
Conversation
…tion, and sync improvements Core changes: - Add zaino-store crate with LMDB-backed block storage (Chain, Freezer) - Bootstrap tool (zaino-admin) for offline import from Zebra DB - Lean formal model for freezer++chain invariants and sync algorithm - Adaptive fetch concurrency and per-phase timing in sync_step - Reorg handling: prev_hash validation, trim tool, snapshot tests - gRPC test tools (zaino-grpc-test, zaino-concurrent-test, zaino-check) - Remove Option<Arc<LmdbStore>> — LMDB is always required - Remove dead StateService backend and unused StoreError variants - Docker: build all workspace binaries, remove BuildKit-only flags - Config: wire start_height through, make -c/--config a global flag - Tracing: add journald support (Linux-gated), log reorgs as warnings - Docs: sync algorithm design doc, benchmarks, grant proposals
|
I'll start with the conflicts, I suppose. |
|
I'll rebase later. Don't worry about it.
…On Tue, Jul 7, 2026 at 3:12 PM Za Wil ***@***.***> wrote:
*zancas* left a comment (zingolabs/zaino#1378)
<#1378 (comment)>
I'll start with the conflicts, I suppose.
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIH3OPWX7DDNI2VNOTL35DSPGLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJQGEYTENBRGI22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-4901124125>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH37COQ4XK2TGHUIISD5DSPGLAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH6EYE5Z5FWR7QKUYWL5DSPGLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJQGEYTENBRGI22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIH6OUULCBISTG7GBQ635DSPGLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJQGEYTENBRGI22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
| Every query takes a `&Snapshot` parameter. `snapshot_nonfinalized_state()` captures | ||
| an `Arc<NonfinalizedBlockCacheSnapshot>` and queries read from that frozen copy. | ||
| This prevents intra-request races at the cost of forcing every method to carry a | ||
| `&Snapshot` parameter — even methods that don't touch non-finalized state (like | ||
| `get_treestate`, which always passes through to the validator). |
There was a problem hiding this comment.
If get_treestate always passes through to the validator and doesn't return tree states that corresponds to the snapshot, that's a bug in get_treestate because the treestate returned will be incompatible with the block that is represented as following it if the snapshot is of a branch that has been reorged relative to the validator's view.
| In the block store, blocks are inserted by hash. The height→hash mapping is | ||
| append-only on the best chain. A block's hash never changes. A height→hash | ||
| lookup returns the same answer forever or returns nothing. No snapshot needed. | ||
| The formal model's `height_consistent` invariant (Section 2 of | ||
| [`BlockStore.lean`](./BlockStore.lean)) ensures every non-genesis block's parent | ||
| exists at `height - 1`, and this invariant is maintained by the ingestion path, | ||
| not by freezing mutable state. |
There was a problem hiding this comment.
This is also incorrect - a client needs a consistent view of the chain as of a particular state, the snapshot. When the client looks up the block hash for a given height as of the snapshot, this needs to be the block height relative to that snapshot's view of the chain tip; if the primary has reorged to a different chain, then getting the block hash for that other chain while using the snapshot will also produce incoherent/invalid history within the client.
|
It is a separate rpc call, isn't it? The chain could get reorged if you
pass a height. By hash it is safe
…On Tue, 7 July 2026, 3:56 pm Kris Nuttycombe, ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In docs/block-store-rationale.md
<#1378 (comment)>:
> +Every query takes a `&Snapshot` parameter. `snapshot_nonfinalized_state()` captures
+an `Arc<NonfinalizedBlockCacheSnapshot>` and queries read from that frozen copy.
+This prevents intra-request races at the cost of forcing every method to carry a
+`&Snapshot` parameter — even methods that don't touch non-finalized state (like
+`get_treestate`, which always passes through to the validator).
If get_treestate always passes through to the validator and doesn't
return tree states that corresponds to the snapshot, that's a bug in
get_treestate because the treestate returned will be incompatible with
the block that is represented as following it if the snapshot is of a
branch that has been reorged relative to the validator's view.
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIH6VH2Y4HHL2SB7W4GT5DSUKXA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4DSNRRGAY2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#pullrequestreview-4642896101>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH6D64UWZDCWE7EQ6WT5DSUKXAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH6M4FELGQIT4UFSBXL5DSUKXA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4DSNRRGAY2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIH7K2PKZ7E2IOW6AVXT5DSUKXA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4DSNRRGAY2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
| Of the 9 `ChainIndex` methods that take a `&Snapshot`, here's what actually | ||
| needs it: | ||
|
|
||
| | Method | Takes `&Snapshot` | Genuine race without it? | | ||
| |---|---|---| | ||
| | `get_block_hash(height)` | Yes | **No** — single lookup, stale the moment it returns | | ||
| | `get_indexed_block_by_height(height)` | Yes | **No** — same | | ||
| | `get_block_range(start, end)` | Yes | **No** — client can't observe inconsistency across stream items | | ||
| | `get_compact_block(height)` | Yes | **No** — single lookup | | ||
| | `get_compact_block_stream(start, end)` | Yes | **No** — same | | ||
| | `get_transaction_status(txid)` | Yes | **No** — hash-based main path; best-chain check is advisory | | ||
| | `find_fork_point(hash)` | Yes | **No** — hash-based walk; best-chain check is advisory | | ||
| | `get_raw_transaction(txid)` | Yes | **No** — only peeks at tip for mempool branch ID | | ||
| | `get_tx_out_set_info` | No (creates its own) | **Yes** — accumulator walk across all heights would corrupt mid-reorg | | ||
|
|
||
| 1 out of 9. And `get_tx_out_set_info` is not a gRPC method — it's an internal | ||
| JSON-RPC handler. The snapshot adds a `&Snapshot` parameter to 8 methods that | ||
| don't need it, purely to satisfy a consistency requirement that doesn't exist. |
There was a problem hiding this comment.
This is incoherent. The whole point of a snapshot is to present a coherent, stable view of the chain as of some particular chain tip. Reads through the snapshot should never return data for an alternate consensus chain, and it should be possible for a client holding a snapshot to make an arbitrary number of queries through the snapshot that all return results consistent to that snapshot until the in-memory handle to the snapshot is released.
|
The compact range is consistent. But if you make several rpc calls, the
chain can reorg in between.
…On Tue, 7 July 2026, 3:59 pm Kris Nuttycombe, ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In docs/block-store-rationale.md
<#1378 (comment)>:
> +In the block store, blocks are inserted by hash. The height→hash mapping is
+append-only on the best chain. A block's hash never changes. A height→hash
+lookup returns the same answer forever or returns nothing. No snapshot needed.
+The formal model's `height_consistent` invariant (Section 2 of
+[`BlockStore.lean`](./BlockStore.lean)) ensures every non-genesis block's parent
+exists at `height - 1`, and this invariant is maintained by the ingestion path,
+not by freezing mutable state.
This is also incorrect - a client needs a consistent view of the chain as
of a particular state, the snapshot. When the client looks up the block
hash for a given height as of the snapshot, this needs to be the block
height relative to that snapshot's view of the chain tip; if the primary
has reorged to a different chain, then getting the block hash for that
other chain while using the snapshot will also produce incoherent/invalid
history within the client.
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIH4BCO2LC5J3JU5HKMT5DSUUJA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TCNBYGU2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#pullrequestreview-4642914854>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH7SBRELBTB5RTX4UOT5DSUUJAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH5PXRSO6KXBQAXVIJL5DSUUJA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TCNBYGU2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIH2UNW7G5NX36ETA3RT5DSUUJA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TCNBYGU2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Yes, the range returned by a streaming call to get compact range follows a
snapshot, even if there is a reorg in the middle of the client fetch.
…On Tue, 7 July 2026, 4:01 pm Kris Nuttycombe, ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In docs/block-store-rationale.md
<#1378 (comment)>:
> +Of the 9 `ChainIndex` methods that take a `&Snapshot`, here's what actually
+needs it:
+
+| Method | Takes `&Snapshot` | Genuine race without it? |
+|---|---|---|
+| `get_block_hash(height)` | Yes | **No** — single lookup, stale the moment it returns |
+| `get_indexed_block_by_height(height)` | Yes | **No** — same |
+| `get_block_range(start, end)` | Yes | **No** — client can't observe inconsistency across stream items |
+| `get_compact_block(height)` | Yes | **No** — single lookup |
+| `get_compact_block_stream(start, end)` | Yes | **No** — same |
+| `get_transaction_status(txid)` | Yes | **No** — hash-based main path; best-chain check is advisory |
+| `find_fork_point(hash)` | Yes | **No** — hash-based walk; best-chain check is advisory |
+| `get_raw_transaction(txid)` | Yes | **No** — only peeks at tip for mempool branch ID |
+| `get_tx_out_set_info` | No (creates its own) | **Yes** — accumulator walk across all heights would corrupt mid-reorg |
+
+1 out of 9. And `get_tx_out_set_info` is not a gRPC method — it's an internal
+JSON-RPC handler. The snapshot adds a `&Snapshot` parameter to 8 methods that
+don't need it, purely to satisfy a consistency requirement that doesn't exist.
This is incoherent. The whole point of a snapshot is to present a
coherent, stable view of the chain as of some particular chain tip. Reads
through the snapshot should never return data for an alternate consensus
chain, and it should be possible for a client holding a snapshot to make an
arbitrary number of queries through the snapshot that all return results
consistent to that snapshot until the in-memory handle to the snapshot is
released.
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIHYFGPNTDCUMQ3QV4RD5DSU5JA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TGMJTGYZ2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#pullrequestreview-4642931363>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH55DQEAGWSZVGIRPW35DSU5JAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH6P4NNGSTVK5ZTXIID5DSU5JA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TGMJTGYZ2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIHYVPK2U2OV7POYUHS35DSU5JA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINRUGI4TGMJTGYZ2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Hey, sorry Ive been slow to take a proper look at this, Ironwood coincided with a holiday so haven't had much extra time. This looks really interesting but I do have several architectural concerns with the design in its current form. I will not repeat the snapshot issues already raised, but they form part of the wider concerns below. 1. Non-finalised stateThe proposed persistent-vector structure may be able to support snapshots, but the current API does not preserve a stable chain view across related requests. This needs to be resolved at the storage and API level, rather than treating snapshots as unnecessary. The current non-finalised state also retains recent non-best-chain blocks. These are needed for chain-tip enumeration, transaction status on forks, fork-point discovery and deterministic reorg handling. The proposed design appears to retain only the current best-chain path, so it is unclear how these behaviours would be preserved. 2. Finalised stateThe proposed database removes much of the integrity model built into the current FinalisedState. Zaino should not perform full consensus validation, but it should verify that the persistent data it owns is internally correct before serving it. The current design includes lightweight checks for record integrity, chain continuity, transaction Merkle roots, transparent spend indexes and, when enabled, transparent address-history indexes. It is also structured to support stronger checks such as FlyClient-style verification. The replacement needs an equivalent corruption and tamper-detection model, potentially using authenticated hashes or another cryptographic verification scheme. There is also no database versioning or migration strategy. Could this backend be introduced through the migration and primary/shadow routing system being completed in PR #1347? That system was designed to allow a new database to be built, validated and promoted while the old database continues serving requests. Relatedly, the current router and capability model are designed to allow different consumers to use different database functionality. For example, Zallet does not need compact blocks. The database is separated into tables and capabilities so different versions or modes can load and serve only the required indexes. This new store may therefore make sense as a replacement for the compact-block-serving portion of the database, while remaining inside a larger versioned database environment containing the additional tables and indexes. 3. API and index scopeThe proposed backend currently looks primarily like a compact-block streamer, while Zaino is being developed as a broader indexing service. The architecture needs to account for:
Finally, this should not introduce a second incompatible finality and fork model. Current dev deliberately separates:
That separation supports much more than snapshotting: it is also required for non-best-chain queries, reorg paths and consistent service behaviour. The new store should either fit beneath that model or provide an equivalent complete replacement for it. Additional indexes that zaino currently serves (independant of zebra):
There are also larger plan to move indexing from zebra into zaino which should be taken into account, currently there is an alpha "transparent address indexing" feature that is not yet in production but should be considered. |
|
Could you clarify the scope of the Lean proofs here? My understanding is that they verify properties of the in-memory chain/vector model, but may not cover the LMDB persistence layer, crash recovery, corruption, migrations, concurrent snapshots, or the wider non-finalised fork model. Are those behaviours included in the formal model, or are they expected to be validated separately? |
|
They cover correctness of ingestion in context of chain reorgs. It assumes that
lmdb transaction model is correct. Snapshot consistency is modeled by the
fact that we use a functional model and implemented by the usage of
persistent data structures.
- the snapshot-aware ChainIndex API;
Since you get data from zebra via a RPC that doesn't allow you to acquire
snapshots, unless I am missing something, you inherit the same (lack of)
guarantee. When the user requests a range of compact blocks, it will be a
snapshot. But if the user calls get range, and get height, it is not
necessary on the same state.
- the JSON-RPC services that Zaino currently serves or is intended to
take over;
- core indexes such as txid -> location and outpoint -> spending
transaction;
- transparent address transactions, balances, UTXOs and deltas;
- experimental and planned wallet indexes;
- transaction status, fork discovery, commitment-tree data and txout-set
information.
Looking roughly at the design/implementation of these APIs, I believe they
could be added too.
However, generally speaking, IMO, I think the current design has/will have
a hard time working in production. The foundation of LWD that Zaino aims to
replace is compact block streaming, and it should be nailed down. The other
services are available from zebra (for the most part), therefore, there is
no urgency in migrating, especially if it impacts the core functionality.
…On Wed, Jul 15, 2026 at 4:15 PM idky137 ***@***.***> wrote:
*idky137* left a comment (zingolabs/zaino#1378)
<#1378 (comment)>
Could you clarify the scope of the Lean proofs here? My understanding is
that they verify properties of the in-memory chain/vector model, but may
not cover the LMDB persistence layer, crash recovery, corruption,
migrations, concurrent snapshots, or the wider non-finalised fork model.
Are those behaviours included in the formal model, or are they expected to
be validated separately?
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIH4HE3F2JR4PJ4MSJFT5E6GYPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJYGE2TQOJZG432M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-4981589977>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH3RPAPT752RM5345F35E6GYPAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH7ZOORJRSCLPHOFUBL5E6GYPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJYGE2TQOJZG432M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIH5UVVJEK5QI6U2HHTL5E6GYPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOJYGE2TQOJZG432M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Hey, I have been exploring whether the im::vector approach here could be adapted to retain the wider ChainIndex requirements, including competing forks and the current snapshot API. The proposed shape is roughly: Branches store only their suffix after the common trunk, rather than duplicating a complete chain. Branches that share part of a suffix can hold clones of the same Arc values and may also share im::Vector structure. When a competing branch becomes best, the implementation constructs a new trunk, converts the old best-chain suffix into a competing branch, renormalises the remaining branch paths against the new trunk, and publishes the complete result through the single ArcSwap. The single publication root is important for snapshot consistency. A caller loads one immutable Arc and can chain several reads or drive a range stream against exactly that state. A reorg publishes a new snapshot without mutating the old one, so existing readers continue observing a coherent historical branch. This should support the existing API efficiently: height and range reads index or iterate the ordered trunk vector; For high-concurrency workloads, readers take no shared lock. Short requests borrow one ArcSwap snapshot, while long-lived streams retain one owned snapshot. The directory does not duplicate block data: it stores another Arc to the same block allocation. The hot read path should borrow the block through the snapshot rather than cloning the Arc per request. I would appreciate feedback on whether this preserves the performance and persistent-snapshot goals of this implementation while providing the fork-aware semantics required by ChainIndex. |
|
Could you describe the "wider ChainIndex requirements, including competing forks and the current snapshot API." ? |
|
Ye sure, for chain branch tracking the main things we need are:
And for snapshotting the requirement, callers must be able to obtain a fixed non-finalised-state snapshot and use it across several related operations. Those operations must all observe the same trunk, branches and tip, even if the live state advances or reorgs concurrently. The intent of the proposed structure is to preserve the persistent, efficient best-chain representation from this PR while adding the bounded fork topology and coherent snapshot semantics. |
|
The previous version of this PR was actually closer to these requirements
than this one.
The idea was to keep a persistent hash map (PHM, hash -> block) in the
recent chain state (C). Since a block cannot change hash, this is an append
only hash map (good for perf). The best chain is a Vector<Hash>. There was
a height -> hashes: Vec<Vec<Hash>> (no need for persistence here, this is
only used in the ingestion thread, for GC). You could add a tips:
Vector<(Hash, Hash, Height)> (it's cheap to walk backward and reverse since
it's done occasionally and up to 100 blocks).
- retaining recently observed competing forks within the configured
reorg window: from tips, get hash, fork, height
- looking up retained blocks by either height or hash: PHM + H2Hashes,
- reporting all retained chain tips and their fork points: tips
- distinguishing transactions or blocks on the active best chain from
those on a competing branch: get the hash, compare with best chain hash
- supporting reorg handling where a competing branch becomes best and
the old best-chain suffix remains available as a non-best branch: during
ingestion, we build a new PHM, H2Hashes, tips instance and ArcSwap it.
During chain shift, we shift H2Hashes, and purge old chain tips.
I moved to a simpler Vector<Block> because then GC old forks is automatic.
…On Tue, Jul 21, 2026 at 6:22 PM idky137 ***@***.***> wrote:
*idky137* left a comment (zingolabs/zaino#1378)
<#1378 (comment)>
Ye sure, for chain branch tracking the main things we need are:
- retaining recently observed competing forks within the configured
reorg window
- looking up retained blocks by either height or hash
- reporting all retained chain tips and their fork points
- distinguishing transactions or blocks on the active best chain from
those on a competing branch
- supporting reorg handling where a competing branch becomes best and
the old best-chain suffix remains available as a non-best branch
And for snapshotting the requirement, callers must be able to obtain a
fixed non-finalised-state snapshot and use it across several related
operations. Those operations must all observe the same trunk, branches and
tip, even if the live state advances or reorgs concurrently.
The intent of the proposed structure is to preserve the persistent,
efficient best-chain representation from this PR while adding the bounded
fork topology and coherent snapshot semantics.
—
Reply to this email directly, view it on GitHub
<#1378?email_source=notifications&email_token=AABWIH5TKJNTZ2F2GI2IDZT5F6KFLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBTGY2DEOJQGA32M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5036429007>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABWIH5XBSVEA4H7WDGFV6L5F6KFLAVCNFSNUABFKJSXA33TNF2G64TZHM3TINJWHAZDIMBVHNEXG43VMU5TIOBSGYYDSNJRGYZ2C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABWIH56Z74SS67D3YW5FG35F6KFLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBTGY2DEOJQGA32M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABWIH24QA6USLME5NWG7WD5F6KFLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBTGY2DEOJQGA32M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
...we shift H2Hashes. We also delete these hashes from the PHM. |
Ahh ok, is this implementation available to view anywhere? I think I missed this as I have been basing review on the current PR. This earlier design looks more like the right foundation for Zaino. However I wonder if it would make sense to try to utilise the benefits of the current A possible shape could be: This keeps the main benefits of your earlier design: forks are represented implicitly through parent hashes, each unique block is stored once, reorgs mainly replace the canonical path and tip metadata, and expired blocks can be removed efficiently using hashes_by_height. The main difference is storing Arc directly in the canonical vector rather than hashes. The vector and PHM reference the same block allocation, so payloads are not duplicated, but best-chain ranges can be served directly without a PHM lookup for every block. This seems worthwhile for Zaino because block ingestion and reorgs are relatively infrequent, while canonical block ranges may be served to thousands of concurrent clients. Publishing the chain, block map and tips through one ArcSwap also preserves the current snapshot semantics: existing readers retain a coherent historical view while a complete new state is constructed and published after an extension, reorg or GC operation. One other area I have been looking into is serving the more complex indexes (outpoint->spending_txid, transparent_address_history, etc ), this is because the max_non_finalised_depth changing from 100 block to 1000 block meas that full nfs searches - ie for transaction status we would currently have to iter through up to 10,000 blocks (max_depth=1000 * max_chain_forks=10), checking all txids in each block for matches - which may be too inefficient when serving thousands of concurrent clients. An idea I have been looking at is separating out the nfs/ChainHead interface into several ports / interfaces, with the non "core" ports building extra indexes in the nfs/ChainHead snapshot to serve these indexes more efficiently. A possible split would be: ChainHeadBlockService - always enabled. Owns the core snapshot state (chain, blocks, tips) and serves block lookup/ranges, tip and fork queries, ingestion, reorgs and GC. ChainHeadTransactionService - feature-gated. Maintains: txid -> [TransactionLocation] This serves raw transaction lookup, transaction status across best and competing chains, and outpoint spender queries without scanning up to ~10,000 retained blocks. Canonicality is determined against the best-chain vector in the same snapshot, so reorgs do not require rebuilding these indexes. ChainHeadTransparentHistoryService - feature-gated. Maintains: transparent_address -> [TransparentHistoryEntry] Each entry records the block, transaction and output/spend event needed to serve address balances, txids, UTXOs and deltas without rescanning the full 1,000-block best chain for every request. For queries below the ChainHead base, the transaction and transparent services would depend on separate driven ports implemented by the consumer, for example: trait ChainHeadTransactionSource { trait ChainHeadTransparentHistorySource { zaino-state could initially implement these adapters using the current finalised state, then switch to the new finalised-state interfaces later. All enabled indexes should be published in the same ArcSwap as the core block state, so every block, tip, transaction and transparent query uses one coherent chain generation. This follows the same ports/adapters and optional-capability structure as the new mempool ( #1416 ) while keeping the core ChainHead usable on its own. |
…tion, and sync improvements
Core changes:
Fixes: